if(LL_VN_Wine ~= nil)
	return
end

local tCork = 3001
local tSpout = 3002

local climbHeight = 512*FRACUNIT // how far above the start height the spout shall climb
local climbProgRateInit = FRACUNIT/4
local climbProgRateRate = 80*FRACUNIT/100 // what the rate is multiplied by each tic to quadrize it
local climbProgRateMin = 3*FRACUNIT/1000 // lowest acceptable rate value to prevent stalls

local corkPopLerpAmp = 48*FRACUNIT
local corkShakeLerpAmp = 16*FRACUNIT
local spoutLerpAmp = 12*FRACUNIT

// denom = how many tics for a full lerp cycle
local corkPopLerpRate = FRACUNIT/63
local corkShakeLerpRate = FRACUNIT/4
local spoutLerpRate = FRACUNIT/8

local corkSectors, spoutSectors
local latestLap = 0
local climbProgress, climbProgressRate
local corkOrigFloorHeight, corkOrigCeilHeight, corkQuarterHeight
local spoutOrigCeilHeight
local corkPopPhase, corkShakePhase, spoutPhase

local function TurnsToSine(phase)
	// turns (0 -> FRACUNIT) into degress (0 -> 360 * FRACUNIT)
	local turnsToDegrees = FixedMul( phase, 360*FRACUNIT )
	
	// degrees into angle ( 0 -> 2 ^ 32 )
	local degreesToAngle = FixedAngle( turnsToDegrees )

	// sin works on angle_t exclusively
	return sin(degreesToAngle)
end

local function AdvancePhase(phase, amount)
	local ret = phase + abs(amount)

	// phases are stored in turns
	// one full turn is FRACUNIT
	if ret > FRACUNIT then
		ret = $ - FRACUNIT
	end

	return ret
end

local function TaggedSectors(tag)
	local list = {}

	for sector in sectors.tagged(tag) do
		table.insert(list, sector)
	end

	return list
end

local function GetTableLength(tbl)
	local count = 0
	
	if (tbl == nil)
		return count
	end
	
	for _ in pairs(tbl) do
		count = $ + 1
	end
	
	return count
end

local function SetFloor(list, height)
	for i = 1, GetTableLength(list) do
		//due to hook differences between OnMapLoad and
		//PostThinkFrame, there's a small chance you'll
		//get invalid sectors at the start of a map
		//i'll just gate here
		if (list[i] ~= nil and list[i].valid ~= false) then
			list[i].floorheight = height
		end
	end
end

local function SetCeiling(list, height)
	for i = 1, GetTableLength(list) do
		//due to hook differences between OnMapLoad and
		//PostThinkFrame, there's a small chance you'll
		//get invalid sectors at the start of a map
		//i'll just gate here
		if (list[i] ~= nil and list[i].valid ~= false) then
			list[i].ceilingheight = height
		end
	end
end

// this used to be a hook in the acs version but that hook doesn't exist in
// lua, so now it is something tracked in an update tic
local function UpdateLap()
	local newLap = latestLap

	for p in players.iterate do
		// skip anyone not playing
		if p.spectator or not (p.mo and p.mo.valid) then
			continue
		end
		
		if p.laps > newLap then
			newLap = p.laps
		end
	end

	if newLap <= latestLap then
		return
	end

	latestLap = newLap // netsync'd to always be the latest lap progressed in the lobby

	if latestLap == 3 then
		S_StartSound(nil, sfx_s3k4b)
		S_StartSound(nil, sfx_s3k46)
		S_StartSound(nil, sfx_s3k4e)
	end
end

//i cannot do this in OnMapLoad exclusively
//why?
//because OnMapLoad only executes when the
//SERVER requests a map load for net sync safety,
//not when a client is forced to load a map upon
//joining a netgame mid-session
//HOWEVER
//i also can't do it in netvars because that hook
//is NOT meant for local cache aggregation in-principle
//AND
//i cant do any gating because the collections are
//entirely local and go out of sync due to a lack
//of focused hooks to manage it right
//so forcibly refresh every tic i guess
local function LazyFindSectors()
	corkSectors = TaggedSectors(tCork)
	spoutSectors = TaggedSectors(tSpout)
end

//setup
local function OnMapLoad()
	LazyFindSectors()
	latestLap = 0
	corkOrigFloorHeight = 0
	corkOrigCeilHeight = 0
	
	if (GetTableLength(corkSectors) > 0) then
		corkOrigFloorHeight = corkSectors[1].floorheight
		corkOrigCeilHeight = corkSectors[1].ceilingheight
	end

	corkQuarterHeight = abs(corkOrigCeilHeight - corkOrigFloorHeight)/4
	spoutOrigCeilHeight = 0
	
	if (GetTableLength(spoutSectors) > 0) then
		spoutOrigCeilHeight = spoutSectors[1].ceilingheight
	end

	climbProgress = 0
	climbProgressRate = climbProgRateInit
	corkPopPhase = 0
	corkShakePhase = 0
	spoutPhase = 0
end

// i need loop control over these sounds, and unfortunately
// the only way to do that is to associate them to an origin
// in the level
// thankfully, its hardcoded such that if the origin is a
// local player's mobj, the sound plays at global volume
local function OnPlayerThink(p)
	if not (p.mo and p.mo.valid) then
		return
	end
	
	if( latestLap == 2 ) then
		S_StopSoundByID( p.mo, sfx_s3k48 )
		
		if not ( S_SoundPlaying( p.mo, sfx_s3k67 ) ) then
			S_StartSound( p.mo, sfx_s3k67, p )
		end
	end
	
	if ( latestLap == 3 ) then
		S_StopSoundByID( p.mo, sfx_s3k67 )
		
		if not ( S_SoundPlaying( p.mo, sfx_s3k48 ) ) then
			S_StartSound( p.mo, sfx_s3k48, p )
		end
	end
end

local function OnPostThinkFrame()
	LazyFindSectors()
	LL_VN_Comms.setField("wine_spoutHeight", nil)
	UpdateLap()

	corkPopPhase = AdvancePhase(corkPopPhase, corkPopLerpRate)
	corkShakePhase = AdvancePhase(corkShakePhase, corkShakeLerpRate)
	spoutPhase = AdvancePhase(spoutPhase, spoutLerpRate)

	if latestLap <= 1 then
		SetFloor(corkSectors, corkOrigFloorHeight)
		SetCeiling(corkSectors, corkOrigCeilHeight)
		SetCeiling(spoutSectors, spoutOrigCeilHeight)
	elseif latestLap == 2 then
		local corkShakeSinInfluence = FixedMul(TurnsToSine(corkShakePhase), corkShakeLerpAmp)

		SetFloor(corkSectors, corkOrigFloorHeight + corkShakeSinInfluence)
		SetCeiling(corkSectors, corkOrigCeilHeight + corkShakeSinInfluence)
	else
		local newCorkFloor = ease.linear(climbProgress, corkOrigFloorHeight, corkOrigFloorHeight + climbHeight)
		local newCorkCeil = ease.linear(climbProgress, corkOrigCeilHeight, corkOrigCeilHeight + climbHeight)
		local newSpoutCeil = ease.linear(climbProgress, spoutOrigCeilHeight, spoutOrigCeilHeight + climbHeight)
		local corkPopSinInfluence = ease.linear(climbProgress, 0, FixedMul(TurnsToSine(corkPopPhase), corkPopLerpAmp))
		local spoutSinInfluence = ease.linear(climbProgress, 0, FixedMul(TurnsToSine(spoutPhase), spoutLerpAmp))

		SetFloor(corkSectors, newCorkFloor + corkPopSinInfluence)
		SetCeiling(corkSectors, newCorkCeil + corkPopSinInfluence)
		local fullNewSpoutCeil = newSpoutCeil + (corkQuarterHeight * 2) + spoutSinInfluence
		SetCeiling(spoutSectors, fullNewSpoutCeil)

		climbProgress = min(FRACUNIT, max(0, $ + climbProgressRate))
		climbProgressRate = max(climbProgRateMin, FixedMul($, climbProgRateRate))
		LL_VN_Comms.setField("wine_spoutHeight", fullNewSpoutCeil)
	end

	return ret
end

local function OnNetVars(net)
	latestLap = net($)
	climbProgress = net($)
	climbProgressRate = net($)
	corkOrigFloorHeight = net($)
	corkOrigCeilHeight = net($)
	corkQuarterHeight = net($)
	spoutOrigCeilHeight = net($)
	corkPopPhase = net($)
	corkShakePhase = net($)
	spoutPhase = net($)
end

local wineLib = {}
wineLib.mapLoad = OnMapLoad
wineLib.playerThink = OnPlayerThink
wineLib.postThink = OnPostThinkFrame
wineLib.netVars = OnNetVars
rawset(_G, "LL_VN_Wine", wineLib)